#pragma once

#include <boost/circular_buffer.hpp>

#include "../utils/pack.h"
#include "../utils/misc.h"

namespace mdtx::online::indicator
{
    template <typename Tin, typename Tout, size_t Nin, size_t Nout>
    class indi_base
    {
    public:
        // input scala type
        using scala_in = Tin;
        // output scala type
        using scala_out = Tout;
        // input tuple
        using tuple_in = ntuple<Tin, Nin>;
        // output tuple
        using tuple_out = ntuple<Tout, Nout>;
        // number of input
        constexpr static size_t num_in = Nin;
        // number of output
        constexpr static size_t num_out = Nout;

    protected:
        constexpr static make_index_range<0, Nin> inidx{};
        constexpr static make_index_range<Nin, Nin + Nout> outidx{};

        template <typename TUPLE, size_t... Ins, size_t... Outs>
        inline void call_update_iter(TUPLE &tup, std::index_sequence<Ins...>, std::index_sequence<Outs...>)
        {
            std::tie(*std::get<Outs>(tup)++...) = update(tuple_in(*std::get<Ins>(tup)++...));
        }

    public:
        virtual tuple_out update(tuple_in const &) = 0;

        template <typename... T>
        tuple_out update(T... in)
        {
            static_assert(sizeof...(T) == Nin, "Incorrect number of input.");
            return update(tuple_in(in...));
        }

        template <typename... Iter>
        void update_n(size_t n, Iter... inout)
        {
            static_assert(sizeof...(Iter) == (Nin + Nout), "Incorrect number of input/output.");
            auto tup = std::tuple(inout...);
            for (size_t i = 0; i < n; ++i)
            {
                call_update_iter(tup, inidx, outidx);
            }
        }

        template <typename... T>
        tuple_out operator()(T... in)
        {
            static_assert(sizeof...(T) == Nin, "Incorrect number of input.");
            return update(tuple_in(in...));
        }

        tuple_out operator()(tuple_in const &in)
        {
            return update(in);
        }

        virtual ~indi_base() = default;
    };

    template <typename T>
    using indi_unary1 = indi_base<T, T, 1, 1>;

    template <typename T>
    using indi_unary2 = indi_base<T, T, 1, 2>;

    template <typename T>
    using indi_weighted1 = indi_base<T, T, 2, 1>;

    template <typename T>
    using indi_weighted2 = indi_base<T, T, 2, 2>;

    template <typename T>
    using indi_binary1 = indi_base<T, T, 2, 1>;

    template <typename T>
    using indi_binary2 = indi_base<T, T, 2, 2>;

    namespace detail
    {
        template <typename UPDATER>
        class indi_wrap_cu : public indi_base<typename UPDATER::scala_in,
                                              typename UPDATER::scala_out,
                                              UPDATER::num_in,
                                              UPDATER::num_out>
        {
            using base = indi_base<typename UPDATER::scala_in,
                                   typename UPDATER::scala_out,
                                   UPDATER::num_in,
                                   UPDATER::num_out>;

        public:
            using typename base::tuple_in;
            using typename base::tuple_out;
            using base::update;

        protected:
            UPDATER updater;

        public:
            template <typename... Args>
            explicit indi_wrap_cu(Args &&...args)
                : updater(std::forward<Args>(args)...)
            {
            }

            tuple_out update(tuple_in const &in) override
            {
                updater.insert(in);
                return updater.get();
            }
        };

        template <typename UPDATER>
        class indi_wrap_ro : public indi_base<typename UPDATER::scala_in,
                                              typename UPDATER::scala_out,
                                              UPDATER::num_in,
                                              UPDATER::num_out>
        {
            using base = indi_base<typename UPDATER::scala_in,
                                   typename UPDATER::scala_out,
                                   UPDATER::num_in,
                                   UPDATER::num_out>;

        protected:
            using base::inidx;

        public:
            using base::num_in;
            using typename base::scala_in;
            using typename base::tuple_in;
            using typename base::tuple_out;
            using base::update;

        protected:
            UPDATER updater;
            boost::circular_buffer<scala_in> buf;

            template <size_t... Is>
            inline void call_roll(tuple_in const &in, std::index_sequence<Is...>)
            {
                updater.roll(std::get<Is>(in)..., buf[Is]...);
            }

            template <size_t... Is>
            inline void call_bpush(tuple_in const &in, std::index_sequence<Is...>)
            {
                bpush(buf, std::get<Is>(in)...);
            }

        public:
            template <typename... Args>
            explicit indi_wrap_ro(ptrdiff_t period, Args &&...args)
                : updater(std::forward<Args>(args)...),
                  buf(period * num_in)
            {
            }

            tuple_out update(tuple_in const &in) override
            {
                if (buf.full())
                {
                    call_roll(in, inidx);
                }
                else
                {
                    updater.insert(in);
                }
                call_bpush(in, inidx);

                return updater.get();
            }
        };
    } // namespace detail

}
